package com.hue.gulimail.gulimailproduct.service.impl;

import com.hue.gulimail.gulimailproduct.entity.CategoryBrandRelationEntity;
import com.hue.gulimail.gulimailproduct.service.CategoryBrandRelationService;
import com.hue.gulimail.gulimailproduct.vo.Catalog2Vo;
import com.hue.gulimail.gulimailproduct.vo.Catalog3Vo;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.TimeUnit;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hue.common.utils.PageUtils;
import com.hue.common.utils.Query;

import com.hue.gulimail.gulimailproduct.dao.CategoryDao;
import com.hue.gulimail.gulimailproduct.entity.CategoryEntity;
import com.hue.gulimail.gulimailproduct.service.CategoryService;
import org.springframework.transaction.annotation.Transactional;

/**
 * @author yfq
 */
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
    @Autowired
    private CategoryBrandRelationService categoryBrandRelationService;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RedissonClient redissonClient;

    @Override
    public PageUtils queryPage(Map<String, Object> params) {
        IPage<CategoryEntity> page = this.page(
                new Query<CategoryEntity>().getPage(params),
                new QueryWrapper<CategoryEntity>()
        );

        return new PageUtils(page);
    }

    /**
     * 查询所有分类，返回树形结构
     *
     * @return
     */
    @Override
    public List<CategoryEntity> listWithTree() {
        List<CategoryEntity> listWithTree = new ArrayList<>();
        Map<Long, CategoryEntity> map = new HashMap();
        List<CategoryEntity> list = list();
        //添加1级分类
        list.forEach((item) -> {
            map.put(item.getCatId(), item);
            if (item.getParentCid() == 0) {
                listWithTree.add(item);
            }
        });
        Set<Map.Entry<Long, CategoryEntity>> entries = map.entrySet();
        for (Map.Entry<Long, CategoryEntity> entity : entries) {
            CategoryEntity categoryEntity = entity.getValue();
            if (categoryEntity.getParentCid() != 0) {
                //当前分类不是1级分类，将该分类加到父分类下
                CategoryEntity parent = map.get(categoryEntity.getParentCid());
                if (parent != null) {
                    parent.addChild(categoryEntity);
                }
            }
        }
//        for (Long key : map.keySet()) {
//            CategoryEntity categoryEntity = map.get(key);
//            //当前分类不是1级分类
//            if (categoryEntity.getParentCid() != 0) {
//                //将该分类加到父分类下
//                CategoryEntity parent = map.get(categoryEntity.getParentCid());
//                if (parent != null) {
//                    parent.addChild(categoryEntity);
//                }
//            }
//        }
        return listWithTree;
    }

    /**
     * 批量删除
     *
     * @param catIds
     */
    @Override
    public void selectRemoveByIds(Long[] catIds) {
        // TODO 选出没有被引用的id

        removeByIds(Arrays.asList(catIds));
    }

    /**
     * 修改数据时如何保证缓存中数据的一致性:
     * 1.双写模式:在想数据库中写入数据时也向缓存中写入数据
     * 2.失效模式:更新数据后使缓存失效
     * 两种模式的问题:可能会出现脏数据
     * 在写少读多的情况下可以加读写锁来保证数据的一致性
     *
     * @param category
     */
    @Override
    @Transactional
//    @CacheEvict(value = "category", key = "'getCategory1'") //删除一个缓存
    //组合两个CacheEvict，更新数据的同时删除多个缓存
    @Caching(evict = {@CacheEvict(value = "category", key = "'getCategory1'"),
            @CacheEvict(value = "category", key = "'catalogs'")})
    //当多个缓存在同一个分区的时候可以使用@CacheEvict的allEntries属性来删除某个分区的所有缓存
    @CacheEvict(value = "category", allEntries = true)
    public void updateDetailById(CategoryEntity category) {
        updateById(category);
        //修改品牌分类关联表,保证冗余字段的一致
        //1.查询所有包含了这个品牌的关联关系
        QueryWrapper<CategoryBrandRelationEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("catelog_id", category.getCatId());
        List<CategoryBrandRelationEntity> categoryBrandRelationList = categoryBrandRelationService.list(wrapper);
        //将分类名重新赋值
        for (CategoryBrandRelationEntity categoryBrandRelationEntity : categoryBrandRelationList) {
            categoryBrandRelationEntity.setCatelogName(category.getName());
        }
        //修改品牌名
        categoryBrandRelationService.updateBatchById(categoryBrandRelationList);
    }

    /**
     * 获取所有的1级分类,将数据缓存到redis中
     *
     * @return
     * @Cacheable: 将方法返回的结果缓存，如果缓存中有数据，那么这个方法将不会执行每个需要缓存的数据都要指定放入那个名字的缓存(缓存的分区按照业务类型来区分)
     * @Caccheable: 的默认行为：1.key默认自动生成，缓存的名字为::SimpleKey[]
     * 2.缓存的值默认使用jdk序列化后存储到缓存中
     * 3.默认的ttl=-1
     * <p>
     * 自定义缓存的配置：
     * 1.自定义缓存的key：接收一个SpEL
     * 2.指定缓存的时间(配置ttl)
     * 3.将缓存的数据保存为json格式
     */
    @Override
    @Cacheable(value = "category", key = "#root.method.name")
    public List<CategoryEntity> getCategory1() {
        System.out.println("catagorys1...");
        QueryWrapper<CategoryEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("parent_cid", 0);
        List<CategoryEntity> categoryList = list(wrapper);
        return categoryList;
    }

    /**
     * 空数据缓存并设置过期时间：解决缓存穿透
     * 对加入缓存的数据设置随机的过期时间：解决缓存雪崩
     * 对热点key加锁：解决缓存击穿
     */
    @Override
    @Cacheable(value = "category", key = "'catalogs'")
    public Map<String, List<Catalog2Vo>> getAllCatalog() {
        //返回的数据
        Map<String, List<Catalog2Vo>> data = null;

        //循环的获取数据,直到获取到数据
        do {
            data = getCatalogJsonFromDBWithRedissonLock();
            //如果没有获取到数据，等待200ms后再次尝试
            if (data == null) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } while (data == null);
        return data;

//        ValueOperations ops = redisTemplate.opsForValue();
        //从redis获取数据
//        String catalogsJson = (String) ops.get("catalogs");

//        if (catalogsJson == null) {
        //从数据库中获取json数据,并放入到缓存
//            getCatalogJsonFromDBWithLocalLock(data);
//            getCatalogJsonFromDBWithRedisLock(data);
//        data = getCatalogJsonFromDBWithRedissonLock();
//        return data;
//        }

        //将获取的数据转换为需要返回的格式
//        TypeReference<Map<String, List<Catalog2Vo>>> typeReference = new TypeReference<Map<String, List<Catalog2Vo>>>() {
//        };
//        data = JSON.parseObject(catalogsJson, typeReference);
//        return data;
    }

    /**
     * 从数据库中查询所有分类信息的json字符串
     * 采用本地锁
     *
     * @return
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithLocalLock() {
        Map<String, List<Catalog2Vo>> data = null;
        synchronized (this) {
            data = getCatalogJsonFromDB();
        }
        return data;
    }

    /**
     * 采用redis的分布式锁(setnx k v)
     * 采用redis分布式锁的两个要点:
     * 1.加锁和设置过期时间要保证原子性
     * 2.判断uuid和删除锁要保证原子性
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedisLock() {
        Map<String, List<Catalog2Vo>> data = null;
        //生成一个uuid在删除锁的时候判断是否删除的是自己的锁
        String uuid = UUID.randomUUID().toString();
        //从redis中获取锁,并且设置过期时间
        Boolean result = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 30, TimeUnit.SECONDS);
        if (result) {
            //成功获取到锁,查询数据
            data = getCatalogJsonFromDB();
            //删除锁,采用lua脚本
            String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then\n" +
                    "    return redis.call(\"del\",KEYS[1])\n" +
                    "else\n" +
                    "    return 0\n" +
                    "end";
            redisTemplate.
                    execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
        } else {
            //没有获取到锁，等待200ms后重试
            try {
                Thread.sleep(200);
                data = getCatalogJsonFromDBWithRedisLock();
            } catch (Exception e) {
            }
        }
        return data;
    }

    /**
     * 采用Redisson分布式锁
     */
    private Map<String, List<Catalog2Vo>> getCatalogJsonFromDBWithRedissonLock() {
        Map<String, List<Catalog2Vo>> data = null;
        //获取redisson分布式锁
        RLock lock = redissonClient.getLock("catalogJson-lock");
        //非阻塞式的尝试去获取锁,返回一个boolean值来判断是否获取到了锁
        boolean isGetLock = lock.tryLock();
        try {
            if (isGetLock) {
                //获取到了锁才查询数据库
                data = getCatalogJsonFromDB();
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (isGetLock) {
                lock.unlock();
            }
        }
        return data;
    }

    /**
     * 从数据库中获取数据
     */
    public Map<String, List<Catalog2Vo>> getCatalogJsonFromDB() {
        Map<String, List<Catalog2Vo>> data = new HashMap<>();
        System.out.println("查询数据库...");
        List<CategoryEntity> catalogList = list();
        Map<Long, CategoryEntity> catalog2Map = new HashMap<>();
        Map<Long, CategoryEntity> catalog3Map = new HashMap<>();
        for (CategoryEntity catalog : catalogList) {
            if (catalog.getCatLevel() == 1) {
                data.put(catalog.getCatId().toString(), new ArrayList<Catalog2Vo>());
            } else if (catalog.getCatLevel() == 2) {
                catalog2Map.put(catalog.getCatId(), catalog);
            } else if (catalog.getCatLevel() == 3) {
                catalog3Map.put(catalog.getCatId(), catalog);
            }
        }

        Map<String, Catalog2Vo> catalog2VoMap = new HashMap();
        Set<Map.Entry<Long, CategoryEntity>> catalog2Entries = catalog2Map.entrySet();
        for (Map.Entry<Long, CategoryEntity> entry : catalog2Entries) {
            CategoryEntity catalog =entry.getValue();
            Catalog2Vo catalog2Vo = new Catalog2Vo();
            catalog2Vo.setCatalog1Id(catalog.getParentCid().toString());
            catalog2Vo.setId(catalog.getCatId().toString());
            catalog2Vo.setName(catalog.getName());
            catalog2Vo.setCatalog3List(new ArrayList<Catalog3Vo>());
            catalog2VoMap.put(catalog.getCatId().toString(), catalog2Vo);
            data.get(catalog.getParentCid().toString()).add(catalog2Vo);
        }
//        for (Long id : catalog2Map.keySet()) {
//            CategoryEntity catalog = catalog2Map.get(id);
//            Catalog2Vo catalog2Vo = new Catalog2Vo();
//            catalog2Vo.setCatalog1Id(catalog.getParentCid().toString());
//            catalog2Vo.setId(catalog.getCatId().toString());
//            catalog2Vo.setName(catalog.getName());
//            catalog2Vo.setCatalog3List(new ArrayList<Catalog3Vo>());
//            catalog2VoMap.put(catalog.getCatId().toString(), catalog2Vo);
//            data.get(catalog.getParentCid().toString()).add(catalog2Vo);
//        }

        Set<Map.Entry<Long, CategoryEntity>> catalog3Entries = catalog3Map.entrySet();
        for (Map.Entry<Long, CategoryEntity> entry : catalog3Entries) {
            CategoryEntity catalog = entry.getValue();
            Catalog3Vo catalog3Vo = new Catalog3Vo();
            catalog3Vo.setId(entry.getKey().toString());
            catalog3Vo.setName(catalog.getName());
            catalog3Vo.setCatalog2Id(catalog.getParentCid().toString());
            catalog2VoMap.get(catalog.getParentCid().toString()).getCatalog3List().add(catalog3Vo);
        }
//        for (Long id : catalog3Map.keySet()) {
//            CategoryEntity catalog = catalog3Map.get(id);
//            Catalog3Vo catalog3Vo = new Catalog3Vo();
//            catalog3Vo.setId(id.toString());
//            catalog3Vo.setName(catalog.getName());
//            catalog3Vo.setCatalog2Id(catalog.getParentCid().toString());
//            catalog2VoMap.get(catalog.getParentCid().toString()).getCatalog3List().add(catalog3Vo);
//        }

//        String dataJson = JSON.toJSONString(data);
        //将数据存到redis,设置随机的过期时间
//        redisTemplate.opsForValue().set("catalogs", dataJson, 20 + (int) (Math.random() * 10), TimeUnit.MINUTES);
        return data;
    }
}
